/* Copyright (c) 2011 Danish Maritime Authority.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dk.dma.ais.tracker.targetTracker;

import com.google.common.util.concurrent.AbstractScheduledService;
import dk.dma.ais.packet.AisPacketSource;
import dk.dma.ais.tracker.targetTracker.TargetTracker.MmsiTarget;
import dk.dma.commons.util.io.IoUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import static java.util.Objects.requireNonNull;

/**
 * Takes care of backing up and restoring in case of a crash.
 *
 * @author Kasper Nielsen
 */
public class TargetTrackerFileBackupService extends AbstractScheduledService {

    /** The logger. */
    private static final Logger LOG = LoggerFactory.getLogger(TargetTrackerFileBackupService.class);

    /** The folder to backup files to. */
    private final Path backupFolder;

    /**
     * The F.
     */
    BackupFile f;

    /** A random string appended to each backup file. To distinguish them from backup files created by former runs. */
    private final String prefix = Integer.toString(ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE), 36)
            .toUpperCase();

    /** The tracker that we are make backups and restoring from. */
    private final dk.dma.ais.tracker.targetTracker.TargetTracker tracker;

    /** A list of those targets that have been backed up. */
    private final Set<dk.dma.ais.tracker.targetTracker.TargetInfo> backedUpTargets = Collections
            .newSetFromMap(new IdentityHashMap<dk.dma.ais.tracker.targetTracker.TargetInfo, Boolean>());

    /**
     * Creates a new backup service.
     *
     * @param tracker      the target tracker that we are make backups and restoring from
     * @param backupFolder the folder to backup and restore from
     */
    public TargetTrackerFileBackupService(dk.dma.ais.tracker.targetTracker.TargetTracker tracker, Path backupFolder) {
        this.tracker = requireNonNull(tracker);
        this.backupFolder = requireNonNull(backupFolder);
    }

    /**
     * Restore backup files.
     *
     * @throws IOException            the io exception
     * @throws ClassNotFoundException the class not found exception
     */
    void restoreBackupFiles() throws IOException, ClassNotFoundException {
        List<Path> paths = new ArrayList<>();
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(backupFolder)) {
            for (Path path : stream) {
                if (path.getFileName().toString().endsWith("-00")) {
                    paths.add(path);
                }
            }
        }
        Path latestFull = IoUtil.findLatestModified(paths);
        if (latestFull != null) {
            LOG.info("Trying to restore backup files " + latestFull);
            String format = latestFull.getFileName().toString();
            format = format.substring(0, format.length() - 3);// strip -00

            ArrayList<Path> files = new ArrayList<>();
            for (Path path : Files.newDirectoryStream(backupFolder)) {
                if (path.getFileName().toString().startsWith(format)) {
                    files.add(path);
                }
            }
            Collections.sort(files); // make sure they are read in the right order

            for (Path path : files) {
                LOG.info("Restoring backup file " + path);
                try (InputStream fos = Files.newInputStream(path);
                        BufferedInputStream bos = new BufferedInputStream(fos);
                        GZIPInputStream gos = new GZIPInputStream(bos);
                        ObjectInputStream oos = new ObjectInputStream(gos)) {
                    AisPacketSource sb = (AisPacketSource) oos.readObject();
                    dk.dma.ais.tracker.targetTracker.TargetInfo ti = (dk.dma.ais.tracker.targetTracker.TargetInfo) oos.readObject();
                    while (sb != null && ti != null) {
                        tracker.update(sb, ti);
                        sb = (AisPacketSource) oos.readObject();
                        ti = (dk.dma.ais.tracker.targetTracker.TargetInfo) oos.readObject();
                    }
                }
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void startUp() throws Exception{
        LOG.info("{} startUp", TargetTrackerFileBackupService.class);
        // If this is the first run, we check if there are any files to restore
        try {
            restoreBackupFiles();
        } catch (Exception e) {
            LOG.error("Cannot restore from backup", e);
        }
        f = new BackupFile();
    }

    /** {@inheritDoc} */
    @Override
    protected void runOneIteration() throws Exception {
        Path p = f.toPath(backupFolder, prefix);
        try {
            if (f.isFull()) {
                backedUpTargets.clear(); // clear all backed up files
            }
            writeBackupFile(p);
            f = f.next();
        } catch (Exception e) {
            LOG.error("Failed to write backup to " + p, e);
            f = f.nextFull();
        }
    }

    /** {@inheritDoc} */
    @Override
    protected Scheduler scheduler() {
        return Scheduler.newFixedRateSchedule(0, 1, TimeUnit.SECONDS);
    }

    private void writeBackupFile(Path p) throws IOException {
        // We write to a temporary file, to make sure we only have complete valid backup files in the folder
        Path temporaryFile = Files.createTempFile(p.getFileName().toString(), ".tmp");
        boolean isFullBackup = true;
        // Serialize all sourceBundle->TargetInfo mappings
        try (OutputStream fos = Files.newOutputStream(temporaryFile);
                BufferedOutputStream bos = new BufferedOutputStream(fos);
                GZIPOutputStream gos = new GZIPOutputStream(bos);
                ObjectOutputStream oos = new ObjectOutputStream(gos)) {
            for (MmsiTarget t : tracker.targets.values()) {
                for (Entry<AisPacketSource, dk.dma.ais.tracker.targetTracker.TargetInfo> e : t.entrySet()) {
                    // Serialize it, if it is a full backup or if the backup flag has not yet been set
                    if (backedUpTargets.add(e.getValue())) {
                        oos.writeObject(e.getKey());
                        oos.writeObject(e.getValue());
                    } else {
                        isFullBackup = false; // at least one file has already been backed up elsewhere
                    }
                }
            }
            // Write footer, 2 nulls mean end of stream
            oos.writeObject(null);
            oos.writeObject(null);
        }

        // move the temporary file to the correct spot
        Files.move(temporaryFile, p);

        // If this backup is a full backup, delete old backup files
        if (isFullBackup) {
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(backupFolder)) {
                for (Path path : stream) {
                    if (!path.equals(p)) { // delete all files but the full backup we just wrote
                        Files.delete(path);
                    }
                }
            }
        }
    }

    /**
     * Wrapping the versioning
     */
    static class BackupFile {
        /**
         * The Major backup version.
         */
        final int majorBackupVersion;
        /**
         * The Minor backup version.
         */
        final int minorBackupVersion;

        /**
         * Instantiates a new Backup file.
         */
        BackupFile() {
            this(1, 0);
        }

        private BackupFile(int major, int minor) {
            this.majorBackupVersion = major;
            this.minorBackupVersion = minor;
        }

        /**
         * Is full boolean.
         *
         * @return the boolean
         */
        boolean isFull() {
            return minorBackupVersion == 0;
        }

        /**
         * Next backup file.
         *
         * @return the backup file
         */
        BackupFile next() {
            return minorBackupVersion == 99 ? nextFull() : new BackupFile(majorBackupVersion, minorBackupVersion + 1);
        }

        /**
         * Next full backup file.
         *
         * @return the backup file
         */
        BackupFile nextFull() {
            return new BackupFile((majorBackupVersion + 1) % 100000, 0);
        }

        /**
         * To path path.
         *
         * @param directory the directory
         * @param prefix    the prefix
         * @return the path
         */
        Path toPath(Path directory, String prefix) {
            return directory.resolve("aisviewer_backup-" + prefix + "-"
                    + new DecimalFormat("00000").format(majorBackupVersion) + "-"
                    + new DecimalFormat("00").format(minorBackupVersion));
        }
    }
}
